<?php
/**
 * article.inc
 * Manage articles.
 * @copyright	Copyright(c) 2002-2010 Bjorn Winberg
 * @author		Bjorn Winberg <cms@anomalye.net>
 * @license		http://www.gnu.org/licenses/gpl.html GNU General Public License version 2
 * @package		AnomalyWiki
 */
class Article extends Module{
	public static $MODULE_UID = 'article';
	const SQL_TABLE_ARTICLE = 'CREATE TABLE IF NOT EXISTS article_items (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, timestamp INTEGER, content TEXT, author_id INTEGER, version INTEGER)';
	const PARAM_MAX_ACTION = 64;
	const PARAM_MAX_ITEM = 64;
	const PARAM_MAX_ITEM_NAME = 1024;
	const PARAM_MAX_BODY = 5000000;
	protected $mSystem;	
	
	/**
	 *
	 */
	public function __construct(){
		parent::__construct();
		$this->loadSettings();
		$this->mSystem = ModuleFactory::loadModule('system');	
	}
	
	/**
	 *
	 */
	public function generateView(){
		$params = array();
		$db = ModuleFactory::getDb();
		$inputAction = $this->getParamEnc('a', 1, self::PARAM_MAX_ACTION);
		$inputItemId = $this->getParamEnc('item', 1, self::PARAM_MAX_ITEM);
		$inputItemName = $this->getParamUId('name', 1, self::PARAM_MAX_ITEM_NAME);
		$currentArticle = $this->getCurrentArticle($db, $inputItemId, $inputItemName);
		$params['article'] = $currentArticle;
		$params['name'] = $_SESSION['article']['currentArticleName'] = $currentArticle !== false? $currentArticle['uid']: $inputItemName;
		$parser = $this->mSystem->mParser;
		if($inputAction === 'edit' || $inputAction === 'edit_form'){
			if(!$this->requireAuthorization('ARTICLE/EDIT')){
				$this->mSystem->feedbackWarn($this->mSystem->localString('ERROR_PERMISSION_DENIED'));
				return false;
			}
			if($_SERVER['REQUEST_METHOD'] === 'POST' && $inputAction === 'edit'){
				$inputBody = $this->getParamEnc('articleBody', 1, self::PARAM_MAX_BODY);
				if(isset($_REQUEST['preview'])){
					$parsedText = $parser->parseText($inputBody, $this->localSetting('ALLOWED_TAGS'), $this->localSetting('ALLOWED_COMMANDS'), false, false, $params['name']);
					$params['preview'] = $this->generatePreview($this->localString('EF_PREVIEWSTART'), $parsedText);
					$params['article']['content'] = $inputBody;
					$parser->triggerFeedback();
					return $this->generateTemplate('article_edit.inc', $params);
				}
				else{
					$parsedText = $parser->parseText($inputBody, $this->localSetting('ALLOWED_TAGS'), $this->localSetting('ALLOWED_COMMANDS'), false, false, $params['name']);
					$sourceText = $parser->getSource();
					$timestamp = time();
					$version = $currentArticle !== false? (int)$currentArticle['version'] + 1: 1;
					$dbValues = array('uid' => $params['name'], 'timestamp' => $timestamp, 'content' => $sourceText, 'author_id' => $_SESSION['auth']['user']['id'], 'version' => $version);
					$db->dbExecute('INSERT INTO article_items' . $db->dbValues($dbValues));
					$parser->triggerFeedback();
					$this->mSystem->feedbackInfo($this->localString('OK_ADD_ARTICLE'));
					$this->mSystem->feedbackLog(static::$MODULE_UID, $this->localString('LOG_EDIT_ARTICLE', array($this->getSafeStr($params['name']))));
					return $parsedText;
				}
			}
			else{
				return $this->generateTemplate('article_edit.inc', $params);
			}
		}
		elseif($inputAction === false){
			if($currentArticle !== false){
				$parsedText = $parser->parseText($currentArticle['content'], $this->localSetting('ALLOWED_TAGS'), $this->localSetting('ALLOWED_COMMANDS'), false, false, $params['name']);
				return $parsedText;
			}
			else{
				$this->mSystem->feedbackWarn($this->localString('ERROR_NOTFOUND', array($this->getSafeStr($params['name']))));
			}
		}
	}
	
	/**
	 *
	 */
	public function firstTimeSetup(){
		$db = ModuleFactory::getDb();
		$db->dbExecute(self::SQL_TABLE_ARTICLE);
		$auth = ModuleFactory::loadModule('auth');
		$editId = $auth->addAction($db, rawurlencode('ARTICLE/EDIT'), 1);
		$auth->grantAction($db, $auth::ADMIN_GROUP_ID, $editId);
	}
	
	/**
	 *
	 */
	 public function handleParseTag($aParser, $aTag){
		$tagBits = explode(rawurlencode($this->mSystem->localSetting('COMMAND_PARAMETER_SEPARATOR')), $aTag);
		$tagCount = count($tagBits);
		$db = ModuleFactory::getDb();
		if($tagBits[0] == ''){
			/*if(isset($tagBits[1]) && isset($tagBits[2]) && ($tagBits[1] == rawurlencode('title') || $tagBits[1] == rawurlencode('keywords') || $tagBits[1] == rawurlencode('description'))){
				$aParseItem['info']['article'][$tagBits[1]] = $tagBits[2];
			}*/
			if($tagCount > 2 && $tagBits[1] == rawurlencode('widget')){
				$widgetName = strtolower($tagBits[2]);
				$contentItem = $this->getCurrentArticle($db, false, $widgetName);
				if($contentItem !== false){
					if($widgetName === $aParser->getItemName() || isset($aParser->mIncludedParents[$widgetName])){
						$aParser->feedbackWarn($this->mSystem->localString('PARSE_LOOP_WARN'));
						return '';
					}
					$tmpParser = new Parser();
					$sourceText = $this->insertVars($contentItem['content'], $tagBits, rawurlencode(ModuleFactory::getSetting('system', 'COMMAND_VARIABLE')));
					$mergedParents = $aParser->mIncludedParents;
					$mergedParents[$aParser->getItemName()] = true;
					$parsedText = $tmpParser->parseText($sourceText, $this->localSetting('ALLOWED_TAGS'), $this->localSetting('ALLOWED_COMMANDS'), false, false, $widgetName, $mergedParents);
					$aParser->mWarnings = array_merge($aParser->mWarnings, $tmpParser->mWarnings);
					$aParser->mInfo = array_merge($aParser->mInfo, $tmpParser->mInfo);
					return $parsedText;
				}
				else{
					$aParser->feedbackWarn($this->mSystem->localString('PARSE_INVALID_VARIABLE', array($this->getSafeStr($widgetName))));
				}
			}
			else{
				$aParser->feedbackWarn($this->mSystem->localString('PARSE_INVALID_TAG'));
			}
		}
		elseif(strpos($tagBits[0], rawurlencode('http://')) === 0){
			$title = $tagBits[0];
			$link = $tagBits[0];
			if(isset($tagBits[1])){
				$title = $tagBits[1];
			}
			return '<a class="parserExternal" href="' . $this->getSafeStr($link) .'">' . $this->getSafeStr($title) . '</a>';
		}
		elseif($aTag == rawurlencode($this->mSystem->localSetting('COMMAND_TAG_START'))){
			return $this->getSafeStr($aTag);
		}
		else{
			$articleName = strtolower($tagBits[0]);
			$title = $tagCount === 1? $tagBits[0]: $tagBits[1];
			$article = $this->getCurrentArticle($db, false, $articleName);
			if($article !== false){
				return '<a class="parserInternal" href="' . $this->generateUrl('article', array('name' => $articleName)) . '">' . $this->getSafeStr($title) . '</a>';
			}
			else{
				return '<a class="parserCreate" href="' . $this->generateUrl('article', array('a' => 'edit', 'name' => $articleName)) . '">' . $this->getSafeStr($title) . '</a>';
			}
		}
		return '';
	 }
	
	/**
	 *
	 */
	private function generatePreview($aTitle, $aContent){
		return '<div id="articleEditPreview"><h2>' . $aTitle . '</h2><div>' . $aContent . '</div></div>';
	}
	
	/**
	 *
	 */
	private function getCurrentArticle($aDb, $aId, &$aName){
		if($aId === false && $aName === false){
			$inputPage = $this->getParamEnc('p', 1, SystemModule::PARAM_MAX_PAGE);
			if($inputPage !== false && isset($_SESSION['article']['currentArticleName'])){
				$aName = $_SESSION['article']['currentArticleName'];
			}
			else{
				$aName = strtolower(rawurlencode($this->localString('DEFAULT_ARTICLE')));
			}
		}
		if($aId !== false){
			if($aId !== ''){
				$listResult = $aDb->dbQuery('SELECT * FROM article_items WHERE id="' . $aDb->dbEscape($aId) . '"');
				if($row = $listResult->fetchArray(SQLITE3_ASSOC)){
					return $row;
				}
			}
			return false; 
		}
		elseif($aName !== false){
			if($aName !== ''){
				$listResult = $aDb->dbQuery('SELECT * FROM article_items WHERE uid="' . $aDb->dbEscape($aName) . '" ORDER BY version DESC LIMIT 1');
				if($row = $listResult->fetchArray(SQLITE3_ASSOC)){
					return $row;
				}
			}
			return false;
		}
	}
		
	/**
	 *
	 */
	private function diffLines($aStrA, $aStrB){
		$aStrA = explode("\n", preg_replace("/\r\n?/", "\n", $aStrA));
		$aStrB = explode("\n", preg_replace("/\r\n?/", "\n", $aStrB));
		return diffEngine($aStrA, $aStrB);
	}
	
	/**
	 * Calculate a diff between two arrays of words, lines or anything else that can be checked for equality.
	 * Reference: "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers.
	 * So far, only the basic O((N+M)D) algorithm is implemented here.
	 */
	private function diffEngine($aArray1, $aArray2){
		$N = count($aArray1);
		$M = count($aArray2);
		$V[1] = 0;
		$vIndex = array();
		for($d = 0; $d <= $N + $M ; $d++ ){
			for($k = -$d ; $k <= $d ; $k += 2){
				if ($k == -$d || ($k != $d && $V[$k - 1] < $V[$k + 1 ])){
					$x = $V[$k + 1];
				}
				else{
					$x = $V[$k - 1] + 1;
				}
				$y = $x - $k;
				while($x < $N && $y < $M && $aArray1[$x] == $aArray2[$y]){
					$x++;
					$y++;
				}
				$V[$k] = $x;
				if($x >= $N && $y >= $M ){
					$vIndex[$d] = $V;
					return diffParseSolution($aArray1, $aArray2, $vIndex);
				}
			}
			$vIndex[$d] = $V;
		}
	}

	/**
	 * Reverse walkthrough to find the complete solution. 
	 */
	private function diffParseSolution($aArray1, $aArray2, $aVIndex){
		$solution = array();
		$count = count($aVIndex);
		for($d = $count - 1, $x = count($aArray1), $y = count($aArray2); $x > 0 || $y > 0; $d--){
			$V = $aVIndex[$d];
			$k = $x - $y;
			$xEnd = $V[$k];
			$yEnd = $x - $k;
			if($k == -$d || ($k != $d && $V[$k - 1] < $V[$k + 1 ])){
				$kPrev = $k + 1;
				$xStart = $V[$kPrev];
				$yStart = $xStart - $kPrev;
				$xMid = $xStart;
				$yMid = $xMid - $k;
			}
			else{
				$kPrev = $k - 1;
				$xStart = $V[$kPrev];
				$yStart = $xStart - $kPrev;
				$xMid =$xStart + 1;
				$yMid = $xMid - $k;
			}
			while($xMid !== $xEnd){
				$solution[] = array('=' => $aArray2[$yEnd - 1]);
				$xEnd--;
				$yEnd--;
			}
			if($xStart == $xMid && $yStart == $yMid - 1){
				$solution[] = array('-' => $aArray2[$yEnd - 1]);
			}
			elseif($xStart == $xMid - 1 && $yStart == $yMid){
				$solution[] = array('+' => $aArray1[$xEnd - 1]);
			}
			$x = $xStart;
			$y = $yStart;
		}
		return array_reverse($solution);
	}
}
?>
